iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0
自我挑戰組

Go in 3o系列 第 19

[Day19] Go in 30 - 介面 - 在函式中活用介面

  • 分享至 

  • xImage
  •  

一、本篇提要

  • 以介面為參數的函式
  • 以介面為回傳值的函式
  • 判斷要不要使用介面作為傳回值
  • 空介面

二、以介面為參數的函式

本篇會透過 io.Reader 為例,用該介面來接收不同型別的值,看看實際運用效果。

以下範例會寫出兩個任務相同的函式,用來解碼筆JSON格式文字,但這兩個函式的參數型別不同,

  1. 一個是字串
  2. 一個是io.Reader介面
    除此之外,前兩筆JSON資料為字串,但第三筆為專案目錄下的data.json,會被 Go 讀取成 File 檔案物件 :
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"
)

type Person struct { //用於json資料的結構
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {

	s := `{"Name":"Joe", "Age":18}`   //第一筆資料
	s2 := `{"Name":"Jane", "Age":21}` //第二筆資料

	p, err := loadPerson(s)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(p)

	//第二筆資料
	// strings.NewReader() 會回傳一個 string.Reader的結構,符合 io.Reader 介面
	p2, err := loadPerson2(strings.NewReader(s2))
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(p2)

	//第三筆資料
	f, err := os.Open("data.json") //開啟同資料夾下的文字檔
	if err != nil {
		fmt.Println(err)
	}

	p3, err := loadPerson2(f)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(p3)
}

// 第一個Json 解析函式,接收字串參數
func loadPerson(s string) (Person, error) {
	var p Person
	err := json.NewDecoder(strings.NewReader(s)).Decode(&p)
	if err != nil {
		return p, err
	}
	return p, nil
}

// 第二個 Json 解析函式,接收io.Reader 介面參數
func loadPerson2(r io.Reader) (Person, error) {
	var p Person
	err := json.NewDecoder(r).Decode(&p)
	if err != nil {
		return p, err
	}
	return p, err
}

json 套件的 NewDecoder() 能夠解析 JSON 資料,他實際上會接收一個io.Reader介面參數,並回傳解碼過的Decoder結構 :

//https://golang.org/pkg/encoding/json/#NewDecoder
func NewDecoder(r io.Reader) *Decoder

然後程式會直接呼叫Decoder的Decode()方法,將資料寫入結構變數的個各欄位。

上面的範例,函式 loadPerson(),會接收一個 string 型別的引數,再將其用 strings.NewReader(),把字串轉成 string.Reader 結構傳給json.NewDecoder(); string.Reader 就是實作了 io.Reader 介面的結構型別。至於在功能完全相同的函式 loadPerson2()中,我們直接接收一個io.Reader介面參數,然後一樣的流程。

有關 io.Reader 介面定義 :

//https://golang.org/pkg/encoding/json/#Reader
type Reader interface {
    Read(p []byte) (n int, err error)
}

而在看看 strings.Reader 和 os.File 定義會發現他們都實作了這個方法:
:

strings.Reader

//https://golang.org/pkg/strings/#Reader
func (r *Reader) Read(b []byte) (n int, err error)

os.File

//https://golang.org/pkg/os/#File
func (r *File) Read(b []byte) (n int, err error)

所以也就是這樣,就解釋了為什麼函式裡的 json.NewDecoder()能接收這些不同的值。

之後當我們在開發API時,使用介面型別作為參數,就意味著使用者傳入的參數資料不會受限於特定型別,可以更加彈性打造物件,只要他們符合規範就好。

三、以介面為回傳值的函式

func someFunc() Speaker() { // 傳回值是 Speaker 介面
    // 程式碼
}

範例說明 :

任何型別只要實作 Error() string 方法,就符合Go語言error介面,事實上每個套件都會定義自己的error,以下我們來看不同套件回傳的error值實際上是什麼型別。

package main

import (
	"encoding/json"
	"fmt"
	"os"
)

type Person struct { //用於json資料的結構
	Name string `json:"name"`
	Age  string `json:"age"` //故意把欄位型別改錯
}

func main() {

	p, err := loadPerson("data.json") //開啟同資料夾下的文字檔
	if err != nil {
		fmt.Printf("%v", err)
		fmt.Printf("%T", err)
	}
	fmt.Println(p)
}

// 第一個Json 解析函式,接收字串參數
func loadPerson(fname string) (Person, error) {
	var p Person
	f, err := os.Open(fname)
	if err != nil {
		return p, err //傳回檔案開啟錯誤
	}
	err = json.NewDecoder(f).Decode(&p)
	if err != nil {
		return p, err //傳回json解析錯誤
	}
	return p, nil
}

結果 :

loadPerson()接收了一個檔,並同時完成讀取和解析JSON檔,但程式中Person結構中Age使用錯誤型別,因此會回傳JSON解析錯誤(*json.UnmarchalTypeError 型別)

data.json改為data1.json,但沒有這個檔案,因此會回傳錯誤(*fs.PathError 型別)

open data1.json: The system cannot find the file specified.*fs.PathError{ }

這顯示Go透過 error 介面傳回不同型別的錯誤。

四、判斷要不要使用介面作為傳回值 ?

分為兩派:

accept interfaces, return structs.

接受介面,返回結構,代表的是接收介面能增加使用者的實作彈性,但傳回值就不該如此,原因為使用者有可能得作額外的型別判斷,花時間查詢文件,才能了解不同欄位及行為差異。

不過另一派抱持相反意見,他們認為API將來可以修改其傳回型別,只要其行為符合介面即可,反而具備更大開發彈性。

實際上,我們還是視情況而定,以下列出一些可以協助我們判斷是否要是用介面作為傳回值的考量點 :

  1. 如果沒絕對必要,就不要在函式傳回介面型別。
  2. 介面定義越精簡越好,好讓使用這去實作他。
  3. 盡量在實質型別(需求)存在之後再撰寫介面,而不是相反。
  4. 通常介面會定義在用到該型別的套件內,對外用不到的就不應該匯出。

五、空介面 interface{}

沒有任何方法集合的介面。

interface{}

我們知道在Go中介面實作是隱性的,而空介面又未指定任何方法,這就意味著 :

Go語言的任何型別都會自動實現空介面,也就是任何型別都能滿足空介面的規範。

這個範例演示函式無如何透過空介面來接收任意型別。

package main

import (
	"fmt"
)

type cat struct {
	name string
}

func main() {

	i := 99
	b := false
	str := "test"
	c := cat{name: "oreo"}
	printDetails(i, b, str, c)
}

func printDetails(data ...interface{}) { //接收數量不定
	for _, i := range data {
		fmt.Printf("%v, %T\n", i, i) //印出質和型別
	}
}

printDetails() 接受不定數量參數 data(成為一個切片),其型別為空介面型別,又剛剛提到任何型別都會自動實作了interface{},於是都可以傳入。

以上就是本篇內容~


上一篇
[Day18] Go in 30 - 介面 - Duck Typing 與 Polymorphism
下一篇
[Day20] Go in 30 - 介面 - 泛型(generic)
系列文
Go in 3o30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言